package jf.jextn.converter;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import jf.jextn.JExtn;
import jf.jextn.annotation.Named;
import jf.jextn.util.ImmutableSimpleEntry;
import jf.jextn.util.ReflectionUtil;

public class Converters {

    private static final Converters instance = new Converters();

    private Map<EntryKey, Converter<?, ?>> registry = new ConcurrentHashMap<>();

    private Map<EntryKey, Converter<?, ?>> cached = new WeakHashMap<>();

    {
        registerConverter(ByteConveter.INSTANCE, Byte.TYPE);
        registerConverter(ShortConveter.INSTANCE, Short.TYPE);
        registerConverter(IntegerConveter.INSTANCE, Integer.TYPE);
        registerConverter(LongConveter.INSTANCE, Long.TYPE);
        registerConverter(FloatConverter.INSTANCE, Float.TYPE);
        registerConverter(DoubleConverter.INSTANCE, Double.TYPE);
        registerConverter(BooleanConverter.INSTANCE, Boolean.TYPE);
        registerConverter(CharacterConverter.INSTANCE, Character.TYPE);
        registerConverter(FileConverter.INSTANCE);

    }

    public static Converters getInstance() {
        return instance;
    }

    public class EntryKey extends ImmutableSimpleEntry<Class<?>, Class<?>> {
        private static final long serialVersionUID = 1L;

        private String name;

        public EntryKey(String name, Class<?> key, Class<?> value) {
            super(key, value);
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public boolean equals(Object o) {
            boolean result = (this == o);
            if (!result) {
                if (this.name != null) {
                    result = name.equals(((EntryKey) o).name);
                } else {
                    result = super.equals(o);
                }
            }

            return result;
        }

        @Override
        public int hashCode() {
            return name != null ? name.hashCode() : super.hashCode();
        }

    }

    public Converters() {
    }

    public <V, O> O convert(V value, Class<O> type, String converterName) {
        Object out = null;
        if (value != null) {
            Class<?> valueType = value.getClass();

            Converter<V, O> converter = findConverter(converterName, valueType, type, true);

            if (converter != null) {
                out = converter.from(value);
            }
        }

        return type.cast(out);
    }

    public <V, O> O convert(V value, Class<O> type) {
        return convert(value, type, null);
    }

    @SuppressWarnings("unchecked")
    protected <V, O> Converter<V, O> findConverter(String name, Class<?> fromType, Class<O> toType, boolean trySwap) {
        EntryKey key = new EntryKey(name, fromType, toType);
        EntryKey originalKey = null;
        Converter<?, ?> c = registry.get(key);
        if (c == null) {
            c = cached.get(key);
            if (c == null) {
                originalKey = key;
                // try without name
                key = new EntryKey(null, fromType, toType);
                c = registry.get(key);
                if (c == null) {
                    c = findEnumConverter(originalKey);
                    if (c == null) {
                        if (toType.isAssignableFrom(fromType)) {
                            c = NullConverter.instance();
                        }
                    }
                    if (c == null) {
                        if (trySwap) {
                            c = findConverter(name, toType, fromType, false);

                            if (c != null) {
                                c = new ObjectToValueConveter<>(c);
                            }
                        }
                    }

                    if (trySwap) {
                        if (c == null) {
                            // We can try to find the converter based on the
                            // toType's super-type. TODO: Implement this feature
                            throw JExtn.throwIAE("no converter found types : {} and {} (or name {}) ", fromType
                                    , toType, name);
                        }

                        cached.put(originalKey, c);
                    }
                }
            }
        }

        return (Converter<V, O>) c;
    }

    private <O extends Enum<O>> Converter<?, ?> findEnumConverter(EntryKey key) {
        Class<?> first = key.getFirst();
        Class<?> second = key.getSecond();

        EnumConverter<O> c = null;

        if (first == String.class && (second.isEnum() || Enum.class.isAssignableFrom(second)))
        {
            @SuppressWarnings("unchecked")
            Class<O> enumType = (Class<O>) second.asSubclass(Enum.class);
            c = new EnumConverter<O>(enumType);
        }

        return c;
    }

    public void registerConverter(Converter<?, ?> converter, Class<?> additionalType) {
        EntryKey key1 = getEntryKey(converter);
        EntryKey key2 = new EntryKey(key1.getName(), additionalType, key1.getSecond());

        converter = decorate(key2, converter);

        putInRegistry(key1, converter, false);
        putInRegistry(key2, converter, false);

    }

    private void putInRegistry(EntryKey key, Converter<?, ?> converter, boolean decorate) {
        if (decorate) {
            converter = decorate(key, converter);
        }

        registry.put(key, converter);
    }

    protected Converter<?, ?> decorate(EntryKey key, Converter<?, ?> converter) {
        if (!(converter instanceof IsNullSafe)) {
            converter = new NullSafeConveterFacade<>(converter);
        } else {
            converter = new ConverterFacade<>(converter);
        }

        return converter;
    }

    public void registerConverter(Converter<?, ?> converter) {
        EntryKey key = getEntryKey(converter);

        registry.put(key, converter);
    }

    private EntryKey getEntryKey(Converter<?, ?> converter) {
        String name = null;
        Class<?> valueType = null, objectType = null;

        if (converter instanceof ProvidesTypeInfo) {
            ProvidesTypeInfo<?, ?> infoProvider = (ProvidesTypeInfo<?, ?>) converter;
            valueType = infoProvider.valueType();
            objectType = infoProvider.objectType();

        } else {
            final Class<?>[] actaulTypes = ReflectionUtil
                    .getActualTypesOfTypeArguments(converter.getClass(), Converter.class);
            if (JExtn.isEmpty(actaulTypes)) {
                throw JExtn.throwIAE("{} does not provide type info", converter);
            }
            valueType = actaulTypes[0];
            objectType = actaulTypes[1];
        }

        Named named = converter.getClass().getAnnotation(Named.class);
        if (named != null) {
            name = named.value();
        }

        EntryKey key = new EntryKey(name, valueType, objectType);
        return key;
    }

}
